<?php

/**
 * MySQL集群代理
 *
 * 本包为程序提供连接MySQL集群的功能，连接上MySQL集群后，所有通过此的查询会被自动进行读写分离。
 *
 * @author    Garbin
 * @package    Library
 * @subpackage Database
 */

/**
 * 获取实例
 *
 * 根据配置项获取实例<br />
 * 配置示例
 * <code>
 * $app->config(array(
 *      // MySQL连接字符集
 *      'mysql_cluster_proxy.charset'   => 'utf8',
 *      // Master连接配置
 *      'mysql_cluster_proxy.master' => array(
 *          'host' => 'master',
 *          'user' => 'root',
 *          'pass' => '',
 *          'name' => 'dbname',
 *          'port' => '3306',
 *      ),
 *      // Slave连接配置，支持多个
 *      'mysql_cluster_proxy.slave' => array(
 *          array(
 *              'host' => 'slave1',
 *              'port' => '3306',
 *              'user' => 'root',
 *              'pass' => '',
 *              'name' => 'dbname',
 *          ),
 *          array(
 *              'host' => 'slave2',
 *              'port' => '3306',
 *              'user' => 'root',
 *              'pass' => '',
 *              'name' => 'dbname',
 *          ),
 *      ),
 * ));
 * </code>
 *
 * @author    Garbin
 * @access public
 * @return Object
 */
function __mysql_cluster_proxy()
{
    static $proxy = null;
    if ($proxy === null)
    {
        $master_config = app_config('mysql_cluster_proxy.master');
        $slave_config  = app_config('mysql_cluster_proxy.slave');
        $charset       = app_config('mysql_cluster_proxy.charset');
        $proxy = new MySQLClusterProxy();
        $proxy->connect($master_config, $slave_config, $charset ? $charset : 'utf8');
    }

    return $proxy;
}

// 使用SparrowMySQL为数据库连接对象
Lib::import('sparrow.mysql');

// 设置数据库连接实例
app_config()->set('model.db', '__mysql_cluster_proxy');

/**
 * MySQL集群代理类
 *
 * 连接至MySQL集群，并自动进行读写分离，符合{@link IDataObject}接口定义
 *
 * @author    Garbin
 * @package    Library
 * @subpackage Database
 */
class MySQLClusterProxy extends Object implements IDataObject
{
    /**
     * DBO存放变量
     */
    protected $_dbo = array(
        'master'    =>  null,
        'slave'    =>  null,
    );

    /**
     * 连接MySQL数据库集群
     *
     * 支持的一主(Master)多从(Slave)的MySQL集群，通过此方法连接至集群。每一个数据库连接实例都应该符合{@link IDataObject}接口定义
     *
     * @author    Garbin
     * @access public
     * @param  array    $master_config    主库连接配置
     * @param  array    $slave_config    从库连接配置，二维数组，可配置多个
     * @param  string   $charset          连接字符集，默认utf8
     * @return bool 成功连接返回true
     */
    public function connect($master_config, $slave_config_list, $charset = 'utf8')
    {
        // 从N个Slave中随机获取一台做为当前查询的数据库服务器，实现请求级别的负载均衡
        $slave_config = $this->_equalizer($slave_config_list);
        $master = $this->_getConnection($master_config, $charset);
        $slave  = $this->_getConnection($slave_config, $charset);

        // 数据库连接对象必须符合IDataObject接口定义
        if (!is_a($master, 'IDataObject') || !is_a($slave, 'IDataObject'))
        {
            throw new Exception('DBO must implement IdataObject interface');
        }

        $this->_dbo = array(
            'master'    => $master,
            'slave'     => $slave,
        );

        return true;
    }

    public function exec($sql)
    {
        $dbo = $this->_dispatchDbo($sql);
        return $dbo->exec($sql);
    }

    public function fetchAll($sql, $index_col = null)
    {
        $dbo = $this->_dispatchDbo($sql);

        return $dbo->fetchAll($sql, $index_col);
    }

    public function fetchOne($sql)
    {
        $dbo = $this->_dispatchDbo($sql);

        return $dbo->fetchOne($sql);
    }

    public function lastInsertId()
    {
        // lastInsertId肯定查Master。因为只有Master才有写操作
        return $this->_dbo['master']->lastInsertId();
    }

    /**
     * 获取数据库连接实例
     *
     * 获取指定的数据库连接实例，从集群中获取主(Master)库连接或从(Slave)库连接
     *
     * @author    Garbin
     * @access public
     * @param string    $dbo_name   指定连接名称，可选值:master, slave。
     * @return Object   SparrowMySQL连接实例
     */
    public function getDbo($dbo_name = 'master')
    {
        return $this->_dbo[$dbo_name];
    }

    /**
     * 根据SQL语句指派数据库连接
     *
     * 分析指定的SQL语句，指定连接实例，实现读写分享
     *
     * @author    Garbin
     * @access protected
     * @param string $sql 指定的SQL语句
     * @return Object SparrowMySQL连接实例
     */
    protected function _dispatchDbo($sql)
    {
        if (strtoupper(substr($sql, 0, 6)) == 'SELECT')
        {
            $dbo_name = 'slave';
        }
        else
        {
            $dbo_name = 'master';
        }

        return $this->_dbo[$dbo_name];
    }

    /**
     * 获取数据库连接实例
     *
     * 获取SparrowMySQL数据库连接实例，连接至数据库。
     *
     * @author    Garbin
     * @access protected
     * @param string  $db_config    数据库连接配置
     * @param string  $charset      MySQL连接字符集，默认为utf8
     * @return Object   返回SparrowMySQL数据库连接实例
     */
    protected function _getConnection($db_config, $charset = 'utf8')
    {
        $db_config['charset'] = $charset;
        $db = new SparrowMySQL();
        $db->connect($db_config);

        return $db;
    }

    /**
     * 查询负载均衡器
     *
     * 随机分配一台Slave做为读服务器
     *
     * @author    Garbin
     * @access protected
     * @param  array    $slave_list     多个Slave的连接配置列表
     * @return array    返回随机选中的一个Slave连接配置
     */
    protected function _equalizer($slave_list)
    {
        $length = count($keys);
        if ($length == 1)
        {
            return current($slave_list);
        }
        $keys = array_keys($slave_list);

        return $slave_list[$keys[rand(0, $length - 1)]];
    }
}
